use strict;
use warnings;

package JasPerl::Util::TimeZone;

use JasPerl::Util::Date;
use JasPerl::Util::Exceptions;

use Scalar::Util qw(blessed);

my $GMT_TZ = JasPerl::Util::TimeZone::Custom->new(0, "GMT");

my $DEFAULT_TZ = eval {
    require DateTime::TimeZone;
    my $tz = DateTime::TimeZone->new(name => 'local');
    JasPerl::Util::TimeZone::DateTime->new($tz);
} || JasPerl::Util::TimeZone::Local->new();

my %TIMEZONES = map { $_->get_id() => $_ } (
    JasPerl::Util::TimeZone::Custom->new(0, "GMT"),
    JasPerl::Util::TimeZone::Custom->new(0, "UTC"),
    $DEFAULT_TZ
);

my %COERCIONS = (
    (__PACKAGE__) => sub {
        my ($class, $from) = @_;
        bless { %{$from} }, $class;
    },
    'DateTime::TimeZone' => sub {
        my ($class, $from) = @_;
        # FIXME: what if $class ne __PACKAGE__
        return JasPerl::Util::TimeZone::DateTime->new($from);
    }
);

#Gets the default TimeZone for this host.
sub get_default {
    return $DEFAULT_TZ->clone();
}

#Sets the TimeZone that is returned by the getDefault method.
sub set_default {
    $DEFAULT_TZ = $_[1]->clone();
}

#Gets the TimeZone for the given ID.
sub get_time_zone {
    my ($class, $id) = @_;
    return $TIMEZONES{$id}->clone() if exists $TIMEZONES{$id};
    return JasPerl::Util::TimeZone::Custom->parse($id) || $TIMEZONES{GMT}->clone();
}

sub new {
    my $class = shift;
    bless { }, $class;
}

sub from {
    my ($class, $from) = @_;
    return unless defined $from;
    return $class->get_time_zone($from) unless ref $from;

    if (blessed $from) {
        if ($from->isa($class)) {
            return $from;
        }
        foreach my $type (keys %COERCIONS) {
            if ($from->isa($type)) {
                return $COERCIONS{$type}->($class, $from);
            }
        }
    }

    JasPerl::Util::CoercionException->throw(
        "Cannot create $class from " . ref $from
    );
}

#Creates a copy of this TimeZone.
sub clone {
    bless { %{$_[0]} }, ref $_[0];
}

#Gets the ID of this time zone.
sub get_id {
    return $_[0]->{id};
}

#Sets the time zone ID.
sub set_id {
    $_[0]->{id} = $_[1];
}

#Returns the offset of this time zone from UTC at the specified time in milliseconds.
sub get_offset {
    return 0;
}

sub get_local_offset {
    return 0;
}

#Queries if the given date is in Daylight Saving Time in this time zone.
sub in_daylight_time {
}

sub use_daylight_time {
}

package # hide from PAUSE
    JasPerl::Util::TimeZone::Custom;
use parent qw(JasPerl::Util::TimeZone);

sub new {
    my ($class, $offset, $id) = @_;
    my $self = $class->SUPER::new();
    $self->set_offset($offset);
    $self->set_id($id);
    return $self;
}

sub parse {
    my ($class, $id) = @_;
    return unless $id =~ /^GMT([+-])(\d{1,2})(?:[:]?(\d{2}))?$/;

    my $offset = $2 * 60 + ($3 || 0);
    my $normid = sprintf("GMT%s%02d:%02d", $1, $offset / 60, $offset % 60);
    $offset = -$offset if $1 eq '-';

    return $class->new($offset * 60_000, $normid);
}

sub get_offset {
    return $_[0]->{offset};
}

sub get_local_offset {
    return $_[0]->{offset};
}

sub set_offset {
    $_[0]->{offset} = $_[1];
}

package # hide from PAUSE
    JasPerl::Util::TimeZone::Local;
use parent qw(JasPerl::Util::TimeZone);

use Time::Local;
use Time::Piece;

sub new {
    my $class = shift;
    my $self = $class->SUPER::new();
    $self->set_id(localtime->strftime('%Z'));
    return $self;
}

sub get_offset {
    my ($self, $time) = @_;
    $time = $time->get_time() if ref $time;
    return localtime($time / 1000)->tzoffset->seconds * 1000;
}

sub get_local_offset {
    my ($self, $year, $mon, $mday, $msec) = @_;
    my $time = timelocal(0, 0, 0, $mday, $mon, $year) * 1000 + $msec;
    return localtime($time / 1000)->tzoffset->seconds * 1000;
}

sub in_daylight_time {
    my ($self, $time) = @_;
    $time = $time->get_time() if ref $time;
    return localtime($time / 1000)->isdst;
}

sub use_daylight_time {
    # TODO
}

package # hide from PAUSE
    JasPerl::Util::TimeZone::DateTime;
use parent qw(JasPerl::Util::TimeZone);

sub _mkdatetime {
    require DateTime;

    my %args = @_;
    if (exists $args{epoch}) {
        return DateTime->from_epoch(%args);
    } else {
        return DateTime->new(%args);
    }
}

sub new {
    my ($class, $tz) = @_;
    my $self = $class->SUPER::new();
    $self->set_id($tz->name);
    $self->{tz} = $tz;
    return $self;
}

sub get_offset {
    my ($self, $time) = @_;
    $time = $time->get_time() if ref $time;
    my $dt = _mkdatetime(epoch => $time / 1000);
    return $self->{tz}->offset_for_datetime($dt) * 1000;
}

sub get_local_offset {
    my ($self, $year, $mon, $mday, $msec) = @_;
    my $dt = _mkdatetime(year => $year, month => $mon + 1, day => $mday);
    $dt->add(seconds => int($msec / 1000), nanoseconds => $msec % 1000 * 1000000);
    return $self->{tz}->offset_for_local_datetime($dt) * 1000;
}

sub in_daylight_time {
    my ($self, $time) = @_;
    $time = $time->get_time() if ref $time;
    my $dt = _mkdatetime(epoch => $time / 1000);
    return $self->{tz}-is_dst_for_datetime($dt);
}

sub use_daylight_time {
    return $_[0]->{tz}->has_dst_changes;
}

1;
